# conda activate learning && uvicorn main:app --host 0.0.0.0 --port 8888 --reload
# lsof -i :8888
# nohup uvicorn main:app --host 0.0.0.0 --port 8888 > custom.log 2>&1 &
import os
import sqlite3
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
import requests
import hashlib
import time
import json
from dotenv import load_dotenv
from urllib.parse import quote
import xml.etree.ElementTree as ET
import secrets
import logging
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.responses import JSONResponse
import base64
from Crypto.Cipher import AES # 需要安装 pycryptodome 库: pip install pycryptodome

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# 加载环境变量
load_dotenv()

# 微信公众号配置
APP_ID = os.getenv("APP_ID")
APP_SECRET = os.getenv("APP_SECRET")

# 微信支付配置
MCH_ID = os.getenv("MCH_ID") # 商户号
API_KEY = os.getenv("API_KEY") # API 密钥
NOTIFY_URL = os.getenv("NOTIFY_URL") # 支付结果通知回调地址

# 课程价格配置
PRICE = float(os.getenv("PRICE", "0.01")) # 普通价格，默认0.01元
PRICE_MEMBER = float(os.getenv("PRICE_MEMBER", "0.01")) # 会员价格，默认0.01元

# 初始化FastAPI应用
@asynccontextmanager
async def lifespan(app: FastAPI):
    if not os.path.exists(DB_FILE):
        conn = get_db_connection()
        cursor = conn.cursor()
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS items (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                description TEXT
            )
        """)
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS enroll (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                company TEXT NOT NULL,
                position TEXT NOT NULL,
                phone TEXT NOT NULL,
                email TEXT,
                order_id TEXT NOT NULL UNIQUE,
                training_class_name TEXT,
                openid TEXT,
                price REAL,
                paied BOOLEAN DEFAULT FALSE,
                status TEXT DEFAULT 'unpaid',
                transaction_id TEXT,
                payment_amount REAL,
                payment_time TIMESTAMP,
                refund_transaction_id TEXT,
                refund_amount REAL,
                refund_time TIMESTAMP,
                is_member BOOLEAN DEFAULT FALSE, -- 新增字段：是否为会员单位
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        conn.commit()
        conn.close()
    yield

import fastapi
app = FastAPI(lifespan=lifespan)
logger.error("FastAPI application started with latest code!")

@app.middleware("http")
async def log_requests(request: Request, call_next):
    logger.info(f"FastAPI Version: {fastapi.__version__}")
    logger.info(f"Middleware: Incoming request path: {request.url.path}")
    response = await call_next(request)
    logger.info(f"Middleware: Outgoing response status: {response.status_code} for path: {request.url.path}")
    return response

@app.exception_handler(HTTPException)
async def custom_http_exception_handler(request: Request, exc: HTTPException):
    logger.error(f"Custom HTTP Exception Handler: Path: {request.url.path}, Status: {exc.status_code}, Detail: {exc.detail}")
    return JSONResponse(
        status_code=exc.status_code,
        content={"message": f"Error: {exc.detail}"}
    )

@app.exception_handler(StarletteHTTPException)
async def custom_starlette_http_exception_handler(request: Request, exc: StarletteHTTPException):
    logger.error(f"Custom Starlette HTTP Exception Handler: Path: {request.url.path}, Status: {exc.status_code}, Detail: {exc.detail}")
    return JSONResponse(
        status_code=exc.status_code,
        content={"message": f"Error: {exc.detail}"}
    )

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

app.mount("/rzwl/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
app.mount("/rzwl/static/js", StaticFiles(directory=os.path.join(BASE_DIR, "static/js")), name="static_js")
app.mount("/rzwl/index", StaticFiles(directory=os.path.join(BASE_DIR, "pages/index")), name="index_assets")
app.mount("/rzwl/testdb", StaticFiles(directory=os.path.join(BASE_DIR, "pages/testdb")), name="testdb_assets")
app.mount("/rzwl/enroll", StaticFiles(directory=os.path.join(BASE_DIR, "pages/enroll")), name="enroll_assets")
app.mount("/rzwl/me/static", StaticFiles(directory=os.path.join(BASE_DIR, "pages/me/static")), name="me_static_assets")
app.mount("/rzwl/pay/static", StaticFiles(directory=os.path.join(BASE_DIR, "pages/pay/static")), name="pay_static_assets")
app.mount("/rzwl/refund/static", StaticFiles(directory=os.path.join(BASE_DIR, "pages/refund/static")), name="refund_static_assets")
app.mount("/rzwl/components/tabbar", StaticFiles(directory=os.path.join(BASE_DIR, "components/tabbar")), name="tabbar_assets")


@app.get("/rzwl/MP_verify_NR5vhRZKq6BhMTTs.txt")
async def serve_wechat_verification_file():
    file_path = os.path.join(BASE_DIR, "授权文件", "MP_verify_NR5vhRZKq6BhMTTs.txt")
    if os.path.exists(file_path):
        with open(file_path, "r") as f:
            content = f.read()
        return HTMLResponse(content, media_type="text/plain")
    raise HTTPException(status_code=404, detail="Verification file not found")


templates = Jinja2Templates(
    directory=os.path.join(BASE_DIR, "pages")
    # 移除 variable_start_string 和 variable_end_string 配置，使用 Jinja2 默认的 {{ }}
    # 这样可以避免与 Vue.js 的 [[ ]] 语法冲突
)

DATABASE_URL = "sqlite:///./train.db"
DB_FILE = os.path.join(BASE_DIR, "train.db")

def get_db_connection():
    conn = sqlite3.connect(DB_FILE)
    conn.row_factory = sqlite3.Row
    return conn

ACCESS_TOKEN_CACHE = {"token": None, "expires_at": 0}
USER_INFO_CACHE = {}

async def get_access_token():
    current_time = time.time()
    if ACCESS_TOKEN_CACHE["token"] and ACCESS_TOKEN_CACHE["expires_at"] > current_time:
        return ACCESS_TOKEN_CACHE["token"]

    url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={APP_ID}&secret={APP_SECRET}"
    response = requests.get(url)
    data = response.json()

    if "access_token" in data:
        ACCESS_TOKEN_CACHE["token"] = data["access_token"]
        ACCESS_TOKEN_CACHE["expires_in"] = data["expires_in"]
        ACCESS_TOKEN_CACHE["expires_at"] = current_time + data["expires_in"] - 60
        return data["access_token"]
    else:
        print(f"获取access_token失败: {data}")
        raise HTTPException(status_code=500, detail=f"获取access_token失败: {data.get('errmsg', '未知错误')}")

async def get_user_info(access_token: str, openid: str):
    if openid in USER_INFO_CACHE:
        return USER_INFO_CACHE[openid]

    url = f"https://api.weixin.qq.com/sns/userinfo?access_token={access_token}&openid={openid}&lang=zh_CN"
    response = requests.get(url)
    response.encoding = 'utf-8'
    data = response.json()

    if "errcode" not in data:
        USER_INFO_CACHE[openid] = data
        return data
    else:
        print(f"获取用户信息失败: {data}")
        raise HTTPException(status_code=500, detail=f"获取用户信息失败: {data.get('errmsg', '未知错误')}")

def generate_sign(data: dict, api_key: str) -> str:
    """生成微信支付签名"""
    stringA = '&'.join(f"{k}={data[k]}" for k in sorted(data) if data[k] is not None and data[k] != '')
    stringSignTemp = f"{stringA}&key={api_key}"
    sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
    return sign

def dict_to_xml(data: dict) -> str:
    """将字典转换为微信支付所需的XML格式"""
    xml = ["<xml>"]
    for k, v in data.items():
        if isinstance(v, str):
            xml.append(f"<{k}><![CDATA[{v}]]></{k}>")
        else:
            xml.append(f"<{k}>{v}</{k}>")
    xml.append("</xml>")
    return "".join(xml)

def xml_to_dict(xml_string: str) -> dict:
    """将微信支付回调的XML转换为字典"""
    root = ET.fromstring(xml_string)
    data = {}
    for child in root:
        data[child.tag] = child.text
    return data

def decrypt_req_info(req_info_encrypted: str, api_key: str) -> dict:
    """解密微信支付退款回调中的req_info字段"""
    try:
        # 微信支付退款回调的req_info解密密钥是API密钥的MD5值
        key_md5 = hashlib.md5(api_key.encode('utf-8')).hexdigest().encode('utf-8')
        
        # req_info是base64编码的
        encrypted_data = base64.b64decode(req_info_encrypted)
        
        # AES-256-ECB模式解密，PKCS7填充
        cipher = AES.new(key_md5, AES.MODE_ECB)
        decrypted_padded_data = cipher.decrypt(encrypted_data)
        
        # 去除PKCS7填充
        # PKCS7填充规则：填充的字节数等于填充值，例如填充5个字节，每个字节都是0x05
        padding_length = decrypted_padded_data[-1]
        decrypted_data = decrypted_padded_data[:-padding_length]
        
        decrypted_xml_string = decrypted_data.decode('utf-8')
        logger.info(f"解密后的req_info XML: {decrypted_xml_string}")
        return xml_to_dict(decrypted_xml_string)
    except Exception as e:
        logger.error(f"解密req_info失败: {e}")
        raise HTTPException(status_code=500, detail=f"解密退款回调信息失败: {e}")

@app.get("/rzwl/wechat/oauth_callback", response_class=RedirectResponse)
async def wechat_oauth_callback(request: Request, code: Optional[str] = None, state: Optional[str] = None):
    if not code:
        raise HTTPException(status_code=400, detail="未获取到微信授权code")

    oauth_url = f"https://api.weixin.qq.com/sns/oauth2/access_token?appid={APP_ID}&secret={APP_SECRET}&code={code}&grant_type=authorization_code"
    response = requests.get(oauth_url)
    oauth_data = response.json()

    if "access_token" not in oauth_data:
        print(f"获取网页授权access_token失败: {oauth_data}")
        raise HTTPException(status_code=500, detail=f"获取网页授权access_token失败: {oauth_data.get('errmsg', '未知错误')}")

    oauth_access_token = oauth_data["access_token"]
    openid = oauth_data["openid"]
    user_info = await get_user_info(oauth_access_token, openid)

    encoded_nickname = quote(user_info.get('nickname', ''))
    encoded_province = quote(user_info.get('province', ''))
    encoded_city = quote(user_info.get('city', ''))
    encoded_country = quote(user_info.get('country', ''))

    base_redirect_path = "/rzwl/me"
    if state == "enroll":
        base_redirect_path = "/rzwl/enroll"
    elif state == "pay":
        base_redirect_path = "/rzwl/pay"

    redirect_to_url = (
        f"{base_redirect_path}?openid={openid}"
        f"&nickname={encoded_nickname}"
        f"&headimgurl={user_info.get('headimgurl', '')}"
        f"&sex={user_info.get('sex', '')}"
        f"&province={encoded_province}"
        f"&city={encoded_city}"
        f"&country={encoded_country}"
    )
    return RedirectResponse(url=redirect_to_url)

class ItemBase(BaseModel):
    name: str
    description: Optional[str] = None

class ItemCreate(ItemBase):
    pass

class Item(ItemBase):
    id: int

    class Config:
        from_attributes = True

class EnrollData(BaseModel):
    name: str
    company: str
    position: str
    phone: str
    email: Optional[str] = None
    training_class_name: Optional[str] = None
    price: Optional[float] = None
    openid: Optional[str] = None
    is_member: Optional[bool] = False # 新增字段：是否为会员单位

class EnrollEntry(EnrollData):
    id: int
    order_id: str
    price: float
    openid: str
    is_member: bool # 新增字段
    paied: bool # 暂时保留，但实际逻辑将由 status 字段控制
    status: str
    transaction_id: Optional[str] = None
    payment_amount: Optional[float] = None
    payment_time: Optional[str] = None # TIMESTAMP 对应 Python 中的字符串
    refund_transaction_id: Optional[str] = None
    refund_amount: Optional[float] = None
    refund_time: Optional[str] = None # TIMESTAMP 对应 Python 中的字符串

    class Config:
        from_attributes = True

@app.get("/rzwl/api/orders/all_paid", response_model=List[EnrollEntry])
async def get_all_paid_orders():
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM enroll WHERE status = 'paid'")
    orders_rows = cursor.fetchall()
    conn.close()
    orders_dicts = [dict(row) for row in orders_rows]
    for order in orders_dicts:
        order['paied'] = bool(order['paied']) # 确保 paied 字段是布尔值
    logger.info(f"API: /rzwl/api/orders/all_paid 被访问，返回 {len(orders_dicts)} 条已支付订单。")
    return orders_dicts

@app.get("/rzwl/api/orders/{openid}", response_model=List[EnrollEntry])
async def get_user_orders(openid: str):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM enroll WHERE openid = ?", (openid,))
    orders_rows = cursor.fetchall()
    conn.close()
    orders_dicts = [dict(row) for row in orders_rows]
    for order in orders_dicts:
        order['paied'] = bool(order['paied'])
    return orders_dicts

@app.get("/rzwl/api/items", response_model=List[Item])
async def read_items():
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM items")
    items_rows = cursor.fetchall()
    conn.close()
    return [dict(row) for row in items_rows]

@app.post("/rzwl/api/items", response_model=Item)
async def create_item(item: ItemCreate):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("INSERT INTO items (name, description) VALUES (?, ?)", (item.name, item.description))
    conn.commit()
    item_id = cursor.lastrowid
    cursor.execute("SELECT * FROM items WHERE id = ?", (item_id,))
    return dict(cursor.fetchone())

@app.post("/rzwl/api/enroll", response_model=EnrollEntry)
async def create_enroll_entry(enroll_data: EnrollData):
    conn = get_db_connection()
    cursor = conn.cursor()
    try:
        order_id = hashlib.md5(f"{enroll_data.phone}-{time.time()}".encode()).hexdigest()
        
        # 根据前端提交的price决定使用哪个价格
        # 注意：前端提交的enroll_data.price应该与PRICE或PRICE_MEMBER之一匹配
        final_price = PRICE
        if enroll_data.price == PRICE_MEMBER:
            final_price = PRICE_MEMBER
        
        cursor.execute(
            "INSERT INTO enroll (name, company, position, phone, email, order_id, training_class_name, price, openid, status, is_member) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (enroll_data.name, enroll_data.company, enroll_data.position, enroll_data.phone,
             enroll_data.email, order_id, enroll_data.training_class_name, final_price, enroll_data.openid, 'unpaid', enroll_data.is_member)
        )
        conn.commit()
        enroll_id = cursor.lastrowid
        cursor.execute("SELECT * FROM enroll WHERE id = ?", (enroll_id,))
        return dict(cursor.fetchone())
    except sqlite3.Error as e:
        conn.rollback()
        raise HTTPException(status_code=500, detail=f"Database error: {e}")
    finally:
        conn.close()

@app.get("/rzwl/api/order/{order_id}")
async def get_order_info(order_id: str):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM enroll WHERE order_id = ?", (order_id,))
    order_row = cursor.fetchone()
    conn.close()
    if order_row:
        return dict(order_row)
    raise HTTPException(status_code=404, detail="Order not found")

class UnifiedOrderRequest(BaseModel):
    order_id: str
    openid: str
    client_ip: str

class UnifiedOrderResponse(BaseModel):
    appId: str
    timeStamp: str
    nonceStr: str
    package: str
    signType: str
    paySign: str

class RefundRequest(BaseModel):
    order_id: str

class RefundResponse(BaseModel):
    return_code: str
    return_msg: str
    result_code: Optional[str] = None
    err_code: Optional[str] = None
    err_code_des: Optional[str] = None
    out_trade_no: Optional[str] = None
    out_refund_no: Optional[str] = None
    refund_id: Optional[str] = None
    refund_fee: Optional[int] = None
    total_fee: Optional[int] = None

@app.post("/rzwl/api/wxpay/unifiedorder", response_model=UnifiedOrderResponse)
async def unified_order(request_data: UnifiedOrderRequest):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM enroll WHERE order_id = ?", (request_data.order_id,))
    order_row = cursor.fetchone()
    conn.close()

    if not order_row:
        raise HTTPException(status_code=404, detail="Order not found")

    order_info = dict(order_row)
    logger.info(f"unified_order: Order Info for order_id {request_data.order_id}: {order_info}")
    if order_info['status'] == 'paid':
        raise HTTPException(status_code=400, detail="订单已支付，无需重复支付。")

    order_info['payment_amount']=order_info['price'] #全额支付
    if order_info['payment_amount'] is None:
        logger.error(f"订单 {request_data.order_id} 的 payment_amount 为 None，无法支付。")
        raise HTTPException(status_code=400, detail="订单支付金额信息缺失，无法支付。")

    # 微信退款金额单位是分
    total_fee = int(order_info['payment_amount'] * 100)
    if total_fee <= 0:
        raise HTTPException(status_code=400, detail="退款金额必须大于0。")

    nonce_str = secrets.token_hex(16)
    timestamp = str(int(time.time()))

    unified_order_data = {
        'appid': APP_ID,
        'mch_id': MCH_ID,
        'nonce_str': nonce_str,
        'body': order_info['training_class_name'] or '培训班报名费',
        'out_trade_no': order_info['order_id'],
        'total_fee': total_fee,
        'spbill_create_ip': request_data.client_ip,
        'notify_url': NOTIFY_URL,
        'trade_type': 'JSAPI',
        'openid': request_data.openid
    }

    sign = generate_sign(unified_order_data, API_KEY)
    unified_order_data['sign'] = sign

    xml_data = dict_to_xml(unified_order_data)
    # print(f"统一下单请求XML: {xml_data}")

    wx_response = requests.post("https://api.mch.weixin.qq.com/pay/unifiedorder", data=xml_data.encode('utf-8'))
    wx_response_data = xml_to_dict(wx_response.text)
    # print(f"统一下单响应: {wx_response_data}")

    if wx_response_data.get('return_code') == 'SUCCESS' and wx_response_data.get('result_code') == 'SUCCESS':
        prepay_id = wx_response_data.get('prepay_id')
        if not prepay_id:
            raise HTTPException(status_code=500, detail="Unified order failed: prepay_id missing")

        # 构建前端JSAPI支付参数
        jsapi_params = {
            'appId': APP_ID,
            'timeStamp': timestamp,
            'nonceStr': nonce_str,
            'package': f'prepay_id={prepay_id}',
            'signType': 'MD5',
        }
        pay_sign = generate_sign(jsapi_params, API_KEY)
        jsapi_params['paySign'] = pay_sign
        return jsapi_params
    else:
        err_msg = wx_response_data.get('return_msg', wx_response_data.get('err_code_des', '未知错误'))
        raise HTTPException(status_code=500, detail=f"Unified order failed: {err_msg}")

@app.post("/rzwl/api/wxpay/refund", response_model=RefundResponse)
async def wxpay_refund(request_data: RefundRequest):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM enroll WHERE order_id = ?", (request_data.order_id,))
    order_row = cursor.fetchone()
    conn.close()

    if not order_row:
        raise HTTPException(status_code=404, detail="Order not found")

    order_info = dict(order_row)
    # 强制将 payment_amount 转换为 float，如果为 None 则默认为 0.0
    order_info['payment_amount'] = float(order_info.get('payment_amount') or 0.0)
    logger.info(f"wxpay_refund: Order Info for order_id {request_data.order_id}: {order_info}")

    if order_info['status'] != 'paid':
        raise HTTPException(status_code=400, detail="订单未支付或状态不正确，无法退款。")

    logger.info('0000000000000000000')
    # 微信退款金额单位是分
    payment_amount_float = order_info['payment_amount']
    logger.info(f"1111111111111111111: {payment_amount_float}")

    # 微信退款金额单位是分
    total_fee = int(payment_amount_float * 100)
    logger.info('2222222222222222222')
    logger.info('total_fee: %s', total_fee)
    if total_fee <= 0:
        raise HTTPException(status_code=400, detail="退款金额必须大于0。")
    refund_fee = total_fee # 全额退款
    logger.info(f"\n\ntotal_fee: {total_fee}, refund_fee: {refund_fee}")
    

    nonce_str = secrets.token_hex(16)
    out_refund_no = f"refund_{order_info['order_id']}_{int(time.time())}" # 退款订单号

    refund_data = {
        'appid': APP_ID,
        'mch_id': MCH_ID,
        'nonce_str': nonce_str,
        'transaction_id': order_info['transaction_id'], # 微信支付订单号
        'out_trade_no': order_info['order_id'], # 商户订单号
        'out_refund_no': out_refund_no, # 商户退款单号
        'total_fee': total_fee,
        'refund_fee': refund_fee,
        'notify_url': NOTIFY_URL, # 退款结果通知回调地址，可以与支付通知地址相同
    }

    sign = generate_sign(refund_data, API_KEY)
    refund_data['sign'] = sign

    xml_data = dict_to_xml(refund_data)
    # print(f"退款请求XML: {xml_data}")

    # 微信退款API需要证书，实际生产环境需要配置requests使用证书
    # cert 参数需要提供证书文件的路径，例如 cert=('/path/to/apiclient_cert.pem', '/path/to/apiclient_key.pem')
    # 请根据您的实际证书路径进行配置
    try:
        wx_response = requests.post(
            "https://api.mch.weixin.qq.com/secapi/pay/refund", 
            data=xml_data.encode('utf-8'),
            cert=(os.path.join(BASE_DIR, '授权文件', 'apiclient_cert.pem'), os.path.join(BASE_DIR, '授权文件', 'apiclient_key.pem'))
        )
        logger.info(f"\n\n\n\n调用微信退款API成功: {wx_response.status_code}")
        wx_response_data = xml_to_dict(wx_response.text)
    except requests.exceptions.RequestException as e:
        logger.error(f"调用微信退款API失败: {e}")
        raise HTTPException(status_code=500, detail=f"调用微信退款API失败: {e}")
    # print(f"退款响应: {wx_response_data}")

    if wx_response_data.get('return_code') == 'SUCCESS' and wx_response_data.get('result_code') == 'SUCCESS':
        refund_id = wx_response_data.get('refund_id')
        refund_amount = int(wx_response_data.get('refund_fee', 0)) / 100.0

        conn = get_db_connection()
        cursor = conn.cursor()
        try:
            cursor.execute(
                "UPDATE enroll SET status = ?, refund_transaction_id = ?, refund_amount = ?, refund_time = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?",
                ('refunded', refund_id, refund_amount, request_data.order_id)
            )
            conn.commit()
            logger.info(f"订单 {request_data.order_id} 退款成功，已更新数据库。")
        except Exception as e:
            conn.rollback()
            logger.error(f"更新退款状态时发生错误: {e}")
            raise HTTPException(status_code=500, detail=f"Database update error: {e}")
        finally:
            conn.close()
        return RefundResponse(
            return_code='SUCCESS',
            return_msg='OK',
            result_code='SUCCESS',
            out_trade_no=request_data.order_id,
            out_refund_no=out_refund_no,
            refund_id=refund_id,
            refund_fee=int(refund_amount * 100),
            total_fee=total_fee
        )
    else:
        err_code = wx_response_data.get('err_code', '未知错误码')
        err_msg = wx_response_data.get('return_msg', wx_response_data.get('err_code_des', '未知错误'))
        logger.error(f"微信退款失败: 订单 {request_data.order_id}, 错误码: {err_code}, 错误信息: {err_msg}")
        raise HTTPException(status_code=500, detail=f"微信退款失败: {err_msg} (错误码: {err_code})")

@app.post("/rzwl/api/wxpay/notify")
async def wxpay_notify(request: Request):
    body = await request.body()
    xml_data = body.decode('utf-8')
    logger.info(f"微信支付回调原始XML数据: {xml_data}")
    notify_data = xml_to_dict(xml_data)
    logger.info(f"微信支付回调通知解析后数据: {notify_data}")

    # 检查顶层通信结果
    if notify_data.get('return_code') != 'SUCCESS':
        err_msg = notify_data.get('return_msg', '通信失败')
        logger.error(f"微信回调通知通信失败: {err_msg}")
        return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[通信失败]]></return_msg></xml>")

    # 区分支付回调和退款回调的逻辑
    if 'req_info' in notify_data: # 认为是退款回调
        logger.info("检测到退款回调通知。")
        try:
            # 对于退款回调，即使顶层result_code不是SUCCESS，也可能在req_info中包含成功信息
            # 所以这里不再检查顶层result_code，而是直接解密req_info并根据其内部状态判断
            decrypted_req_info = decrypt_req_info(notify_data['req_info'], API_KEY)
            logger.info(f"解密后的退款信息: {decrypted_req_info}")

            refund_status = decrypted_req_info.get('refund_status')
            out_trade_no = decrypted_req_info.get('out_trade_no')
            refund_id = decrypted_req_info.get('refund_id')
            refund_fee = int(decrypted_req_info.get('refund_fee', 0)) / 100.0 # 分转元
            
            if refund_status == 'SUCCESS':
                conn = get_db_connection()
                cursor = conn.cursor()
                try:
                    cursor.execute("SELECT * FROM enroll WHERE order_id = ?", (out_trade_no,))
                    order_row = cursor.fetchone()

                    if not order_row:
                        logger.error(f"退款订单 {out_trade_no} 不存在")
                        return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>")

                    if order_row['status'] != 'refunded': # 避免重复处理
                        cursor.execute(
                            "UPDATE enroll SET status = ?, refund_transaction_id = ?, refund_amount = ?, refund_time = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?",
                            ('refunded', refund_id, refund_fee, out_trade_no)
                        )
                        conn.commit()
                        logger.info(f"订单 {out_trade_no} 退款成功，已更新数据库。")
                    else:
                        logger.info(f"订单 {out_trade_no} 状态已为 {order_row['status']}，无需重复处理。")

                    logger.info(f"微信退款回调通知成功处理: 订单 {out_trade_no} 退款状态已更新。")
                    return HTMLResponse("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>")
                except Exception as e:
                    conn.rollback()
                    logger.error(f"处理退款通知时更新数据库发生错误: {e}")
                    return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[内部错误]]></return_msg></xml>")
                finally:
                    conn.close()
            else:
                logger.warning(f"退款失败或异常: 订单 {out_trade_no}, 状态: {refund_status}")
                # 可以根据需要处理退款失败的情况，例如记录日志或通知管理员
                return HTMLResponse("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>") # 即使失败也返回成功，避免微信重复通知
        except HTTPException as e:
            logger.error(f"处理退款回调失败: {e.detail}")
            return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理退款回调失败]]></return_msg></xml>")
        except Exception as e:
            logger.error(f"处理退款回调时发生未知错误: {e}")
            return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[内部错误]]></return_msg></xml>")
    
    else: # 认为是支付回调
        # 支付回调需要验证签名和顶层result_code
        received_sign = notify_data.get('sign')
        sign_data = {k: v for k, v in notify_data.items() if k != 'sign'}
        calculated_sign = generate_sign(sign_data, API_KEY)

        if not received_sign or calculated_sign != received_sign:
            logger.error(f"支付回调签名验证失败: 计算出的签名 {calculated_sign} 与接收到的签名 {received_sign} 不匹配。")
            return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>")

        logger.info("支付回调签名验证成功")
        
        if notify_data.get('result_code') == 'SUCCESS': # 仅检查业务结果
            order_id = notify_data.get('out_trade_no')
            transaction_id = notify_data.get('transaction_id')
            total_fee = int(notify_data.get('total_fee')) # 分

            conn = get_db_connection()
            cursor = conn.cursor()
            try:
                cursor.execute("SELECT * FROM enroll WHERE order_id = ?", (order_id,))
                order_row = cursor.fetchone()

                if not order_row:
                    logger.error(f"订单 {order_id} 不存在")
                    return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>")

                # 检查金额是否匹配
                db_price_fen = int(order_row['price'] * 100)
                if db_price_fen != total_fee:
                    logger.error(f"金额不匹配: 数据库 {db_price_fen} 分, 微信 {total_fee} 分")
                    return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[金额不匹配]]></return_msg></xml>")

                if order_row['status'] == 'unpaid':
                    cursor.execute(
                        "UPDATE enroll SET status = ?, transaction_id = ?, payment_amount = ?, payment_time = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE order_id = ?",
                        ('paid', transaction_id, total_fee / 100.0, order_id)
                    )
                    conn.commit()
                    logger.info(f"订单 {order_id} 支付成功，已更新数据库。")
                else:
                    logger.info(f"订单 {order_id} 状态为 {order_row['status']}，无需重复处理。")

                return HTMLResponse("<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>")
            except Exception as e:
                conn.rollback()
                logger.error(f"处理支付通知时发生错误: {e}")
                return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[内部错误]]></return_msg></xml>")
            finally:
                conn.close()
        else:
            err_msg = notify_data.get('return_msg', notify_data.get('err_code_des', '未知错误'))
            logger.error(f"微信支付通知业务失败: {err_msg}")
            return HTMLResponse("<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理失败]]></return_msg></xml>")

@app.get("/rzwl/", response_class=HTMLResponse)
async def read_root(request: Request):
    return templates.TemplateResponse("index/index.html", {"request": request, "APP_ID": APP_ID})

# ✅ 明确定义 /rzwl/me 路由，确保它在通配符之前注册
@app.get("/rzwl/me", response_class=HTMLResponse)
async def read_me_page(request: Request):
    return templates.TemplateResponse("me/index.html", {"request": request, "APP_ID": APP_ID})

@app.get("/rzwl/testdb", response_class=HTMLResponse)
async def read_test_db_page(request: Request):
    return templates.TemplateResponse("testdb/index.html", {"request": request})

@app.get("/rzwl/enroll", response_class=HTMLResponse)
async def read_enroll_page(request: Request):
    return templates.TemplateResponse("enroll/index.html", {"request": request, "APP_ID": APP_ID, "PRICE": PRICE, "PRICE_MEMBER": PRICE_MEMBER})

@app.get("/rzwl/pay", response_class=HTMLResponse)
async def read_pay_page(request: Request):
    return templates.TemplateResponse("pay/index.html", {"request": request, "APP_ID": APP_ID})

@app.get("/rzwl/refund", response_class=HTMLResponse)
async def read_refund_page(request: Request):
    logger.info('/rzwl/refund - 进入退款页面')
    return templates.TemplateResponse("refund/index.html", {"request": request})

# ⚠️ 通配路由必须放在最后
@app.get("/rzwl/{page_folder}", response_class=HTMLResponse)
async def read_page(page_folder: str, request: Request):
    template_path = os.path.join(page_folder, "index.html")
    try:
        return templates.TemplateResponse(template_path, {"request": request})
    except Exception:
        raise HTTPException(status_code=404, detail="Page not found")

@app.get("/{full_path:path}", include_in_schema=False)
async def catch_all(request: Request, full_path: str):
    logger.error(f"Catch-all route triggered for path: /{full_path}. This path was not explicitly handled.")
    raise HTTPException(status_code=404, detail=f"Path /{full_path} not found by catch-all.")
